/*-------------------------------------------------------------------------
 *
 * ginget.c
 *	  从 GIN 扫描中获取元组。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *			src/backend/access/gin/ginget.c
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/gin_private.h"
#include "access/relscan.h"
#include "common/pg_prng.h"
#include "miscadmin.h"
#include "storage/predicate.h"
#include "utils/datum.h"
#include "utils/memutils.h"
#include "utils/rel.h"

/* GUC 参数 */
int			GinFuzzySearchLimit = 0;

typedef struct pendingPosition
{
	Buffer		pendingBuffer;
	OffsetNumber firstOffset;
	OffsetNumber lastOffset;
	ItemPointerData item;
	bool	   *hasMatchKey;
} pendingPosition;


/*
 * 如果当前偏移量超出界限，转到下一页
 */
static bool fc_moveRightIfItNeeded(GinBtreeData *fc_btree, GinBtreeStack *fc_stack, Snapshot fc_snapshot)
{
	Page		fc_page = BufferGetPage(fc_stack->buffer);

	if (fc_stack->off > PageGetMaxOffsetNumber(fc_page))
	{
		/*
		 * 我们扫描了整页，所以我们应该选择正确的页面
		 */
		if (GinPageRightMost(fc_page))
			return false;		/* 没有更多页面 */

		fc_stack->buffer = ginStepRight(fc_stack->buffer, fc_btree->index, GIN_SHARE);
		fc_stack->blkno = BufferGetBlockNumber(fc_stack->buffer);
		fc_stack->off = FirstOffsetNumber;
		PredicateLockPage(fc_btree->index, fc_stack->blkno, fc_snapshot);
	}

	return true;
}

/*
 * 扫描发布树的所有页面并将所有堆 ItemPointers
 * 保存到 scanEntry->matchBitmap 中
 */
static void fc_scanPostingTree(Relation fc_index, GinScanEntry fc_scanEntry,
				BlockNumber fc_rootPostingTree, Snapshot fc_snapshot)
{
	GinBtreeData fc_btree;
	GinBtreeStack *fc_stack;
	Buffer		fc_buffer;
	Page		fc_page;

	/* 下降到最左边的叶子页面 */
	fc_stack = ginScanBeginPostingTree(&fc_btree, fc_index, fc_rootPostingTree, fc_snapshot);
	fc_buffer = fc_stack->buffer;

	IncrBufferRefCount(fc_buffer); /* 防止在 freeGinBtreeStack 中取消锁定 */

	freeGinBtreeStack(fc_stack);

	/*
	 * 循环遍历发布树的所有叶子页面
	 */
	for (;;)
	{
		fc_page = BufferGetPage(fc_buffer);
		if ((GinPageGetOpaque(fc_page)->flags & GIN_DELETED) == 0)
		{
			int			fc_n = GinDataLeafPageGetItemsToTbm(fc_page, fc_scanEntry->matchBitmap);

			fc_scanEntry->predictNumberResult += fc_n;
		}

		if (GinPageRightMost(fc_page))
			break;				/* 没有更多页面 */

		fc_buffer = ginStepRight(fc_buffer, fc_index, GIN_SHARE);
	}

	UnlockReleaseBuffer(fc_buffer);
}

/*
 * 将所有匹配搜索项的堆元组的 TID 收集到 scanEntry->matchBitmap 中。
 * 这支持三种不同的匹配模式：
 *
 * 1. 部分匹配支持：从当前点扫描，直到
 *	  comparePartialFn 说完成。
 * 2. SEARCH_MODE_ALL：从当前点扫描（这应该是当前 attnum 的第一个
 *	  键）直到遇到 null 项或 attnum 结束
 * 3. SEARCH_MODE_EVERYTHING：从当前点扫描（这应该是当前 attnum 的第一个
 *	  键）直到达到 attnum 结束
 *
 * 如果完成则返回 true，如果需要从头开始重新扫描则返回 false
 */
static bool fc_collectMatchBitmap(GinBtreeData *fc_btree, GinBtreeStack *fc_stack,
				   GinScanEntry fc_scanEntry, Snapshot fc_snapshot)
{
	OffsetNumber fc_attnum;
	Form_pg_attribute fc_attr;

	/* 初始化空的位图结果 */
	fc_scanEntry->matchBitmap = tbm_create(work_mem * 1024L, NULL);

	/* 空查询无法部分匹配任何内容 */
	if (fc_scanEntry->isPartialMatch &&
		fc_scanEntry->queryCategory != GIN_CAT_NORM_KEY)
		return true;

	/* 为键列定位 tupdesc 条目（用于 attbyval/attlen 数据） */
	fc_attnum = fc_scanEntry->attnum;
	fc_attr = TupleDescAttr(fc_btree->ginstate->origTupdesc, fc_attnum - 1);

	/*
	 * 谓词锁条目叶子页面，后续页面将由
	 * moveRightIfItNeeded() 锁定
	 */
	PredicateLockPage(fc_btree->index,
					  BufferGetBlockNumber(fc_stack->buffer),
					  fc_snapshot);

	for (;;)
	{
		Page		fc_page;
		IndexTuple	fc_itup;
		Datum		fc_idatum;
		GinNullCategory fc_icategory;

		/*
		 * stack->off 指向相关条目，缓冲区已经被锁定
		 */
		if (fc_moveRightIfItNeeded(fc_btree, fc_stack, fc_snapshot) == false)
			return true;

		fc_page = BufferGetPage(fc_stack->buffer);
		TestForOldSnapshot(fc_snapshot, fc_btree->index, fc_page);
		fc_itup = (IndexTuple) PageGetItem(fc_page, PageGetItemId(fc_page, fc_stack->off));

		/*
		 * 如果元组存储了另一个属性，则停止扫描
		 */
		if (gintuple_get_attrnum(fc_btree->ginstate, fc_itup) != fc_attnum)
			return true;

		/* 安全地获取属性值 */
		fc_idatum = gintuple_get_key(fc_btree->ginstate, fc_itup, &fc_icategory);

		/*
		 * 检查适当的扫描停止条件
		 */
		if (fc_scanEntry->isPartialMatch)
		{
			int32		fc_cmp;

			/*
			 * 在部分匹配中，遇到任何 null（包括
			 * 占位符）时停止扫描；部分匹配永远不匹配 null
			 */
			if (fc_icategory != GIN_CAT_NORM_KEY)
				return true;

			/*----------
			 * 部分匹配的检查。
			 * 情况 cmp == 0 => 匹配
			 * 情况 cmp > 0 => 不匹配并结束扫描
			 * 情况 cmp < 0 => 不匹配并继续扫描
			 *----------
			 */
			fc_cmp = DatumGetInt32(FunctionCall4Coll(&fc_btree->ginstate->comparePartialFn[fc_attnum - 1],
												  fc_btree->ginstate->supportCollation[fc_attnum - 1],
												  fc_scanEntry->queryKey,
												  fc_idatum,
												  UInt16GetDatum(fc_scanEntry->strategy),
												  PointerGetDatum(fc_scanEntry->extra_data)));

			if (fc_cmp > 0)
				return true;
			else if (fc_cmp < 0)
			{
				fc_stack->off++;
				continue;
			}
		}
		else if (fc_scanEntry->searchMode == GIN_SEARCH_MODE_ALL)
		{
			/*
			 * 在 ALL 模式下，我们对 null 项不感兴趣，因此如果我们
			 * 到达 null 项占位符（这将是给定 attnum 的最后一个条目），
			 * 可以停止。我们确实想包括 NULL_KEY
			 * 和 EMPTY_ITEM 条目。
			 */
			if (fc_icategory == GIN_CAT_NULL_ITEM)
				return true;
		}

		/*
		 * 好的，我们想要返回此条目中列出的 TID。
		 */
		if (GinIsPostingTree(fc_itup))
		{
			BlockNumber fc_rootPostingTree = GinGetPostingTree(fc_itup);

			/*
			 * 在树扫描过程中，我们应该解锁当前页面（但不取消锁定）
			 * 以防止与清理过程发生死锁。
			 *
			 * 我们保存当前条目值（idatum），以便在重新锁定后能够重新找到我们的
			 * 元组
			 */
			if (fc_icategory == GIN_CAT_NORM_KEY)
				fc_idatum = datumCopy(fc_idatum, fc_attr->attbyval, fc_attr->attlen);

			LockBuffer(fc_stack->buffer, GIN_UNLOCK);

			/*
			 * 在发布树上获取谓词锁。我们已经在条目页面上持有锁，
			 * 但对发布树的插入不检查该级别的冲突。
			 */
			PredicateLockPage(fc_btree->index, fc_rootPostingTree, fc_snapshot);

			/* 收集此条目发布树中的所有 TID */
			fc_scanPostingTree(fc_btree->index, fc_scanEntry, fc_rootPostingTree,
							fc_snapshot);

			/*
			 * 我们再次锁定条目页面，在此期间如果被解锁可能发生插入，
			 * 所以我们需要重新找到我们的定位。
			 */
			LockBuffer(fc_stack->buffer, GIN_SHARE);
			fc_page = BufferGetPage(fc_stack->buffer);
			if (!GinPageIsLeaf(fc_page))
			{
				/*
				 * 根页面在我们解锁时变为非叶子节点。我们将重新开始，这种情况并不常见 - 根节点在索引的生命周期内只能变成非叶子节点一次。
				 */
				return false;
			}

			/* 向前搜索以重新查找 idatum */
			for (;;)
			{
				if (fc_moveRightIfItNeeded(fc_btree, fc_stack, fc_snapshot) == false)
					ereport(ERROR,
							(errcode(ERRCODE_INTERNAL_ERROR),
							 errmsg("failed to re-find tuple within index \"%s\"",
									RelationGetRelationName(fc_btree->index))));

				fc_page = BufferGetPage(fc_stack->buffer);
				fc_itup = (IndexTuple) PageGetItem(fc_page, PageGetItemId(fc_page, fc_stack->off));

				if (gintuple_get_attrnum(fc_btree->ginstate, fc_itup) == fc_attnum)
				{
					Datum		fc_newDatum;
					GinNullCategory fc_newCategory;

					fc_newDatum = gintuple_get_key(fc_btree->ginstate, fc_itup,
												&fc_newCategory);

					if (ginCompareEntries(fc_btree->ginstate, fc_attnum,
										  fc_newDatum, fc_newCategory,
										  fc_idatum, fc_icategory) == 0)
						break;	/* 找到了！ */
				}

				fc_stack->off++;
			}

			if (fc_icategory == GIN_CAT_NORM_KEY && !fc_attr->attbyval)
				pfree(DatumGetPointer(fc_idatum));
		}
		else
		{
			ItemPointer fc_ipd;
			int			fc_nipd;

			fc_ipd = ginReadTuple(fc_btree->ginstate, fc_scanEntry->attnum, fc_itup, &fc_nipd);
			tbm_add_tuples(fc_scanEntry->matchBitmap, fc_ipd, fc_nipd, false);
			fc_scanEntry->predictNumberResult += GinGetNPosting(fc_itup);
			pfree(fc_ipd);
		}

		/*
		 * 完成此条目，转到下一个
		 */
		fc_stack->off++;
	}
}

/*
 * Start* 函数设置搜索的初始状态：找到正确的缓冲区并固定它。
 */
static void fc_startScanEntry(GinState *fc_ginstate, GinScanEntry fc_entry, Snapshot fc_snapshot)
{
	GinBtreeData fc_btreeEntry;
	GinBtreeStack *fc_stackEntry;
	Page		fc_page;
	bool		fc_needUnlock;

restartScanEntry:
	fc_entry->buffer = InvalidBuffer;
	ItemPointerSetMin(&fc_entry->curItem);
	fc_entry->offset = InvalidOffsetNumber;
	if (fc_entry->list)
		pfree(fc_entry->list);
	fc_entry->list = NULL;
	fc_entry->nlist = 0;
	fc_entry->matchBitmap = NULL;
	fc_entry->matchResult = NULL;
	fc_entry->reduceResult = false;
	fc_entry->predictNumberResult = 0;

	/*
	 * 我们应该找到条目，并开始扫描发布树或只是在内存中存储发布列表
	 */
	ginPrepareEntryScan(&fc_btreeEntry, fc_entry->attnum,
						fc_entry->queryKey, fc_entry->queryCategory,
						fc_ginstate);
	fc_stackEntry = ginFindLeafPage(&fc_btreeEntry, true, false, fc_snapshot);
	fc_page = BufferGetPage(fc_stackEntry->buffer);

	/* ginFindLeafPage() 将已经检查快照年龄。 */
	fc_needUnlock = true;

	fc_entry->isFinished = true;

	if (fc_entry->isPartialMatch ||
		fc_entry->queryCategory == GIN_CAT_EMPTY_QUERY)
	{
		/*
		 * btreeEntry.findItem 定位第一个 >= 给定搜索键的条目。
		 * (对于 GIN_CAT_EMPTY_QUERY，它将找到最左边的索引条目
		 * 因为 GIN_CAT_EMPTY_QUERY 类别代码的分配方式。) 我们从这里向前扫描并收集所有 TID，以便于条目类型。
		 */
		fc_btreeEntry.findItem(&fc_btreeEntry, fc_stackEntry);
		if (fc_collectMatchBitmap(&fc_btreeEntry, fc_stackEntry, fc_entry, fc_snapshot)
			== false)
		{
			/*
			 * GIN 树被严重重构，因此我们将清理所有找到的数据并重新扫描。有关 'return false' 的注释见 collectMatchBitmap() 中。
			 */
			if (fc_entry->matchBitmap)
			{
				if (fc_entry->matchIterator)
					tbm_end_iterate(fc_entry->matchIterator);
				fc_entry->matchIterator = NULL;
				tbm_free(fc_entry->matchBitmap);
				fc_entry->matchBitmap = NULL;
			}
			LockBuffer(fc_stackEntry->buffer, GIN_UNLOCK);
			freeGinBtreeStack(fc_stackEntry);
			goto restartScanEntry;
		}

		if (fc_entry->matchBitmap && !tbm_is_empty(fc_entry->matchBitmap))
		{
			fc_entry->matchIterator = tbm_begin_iterate(fc_entry->matchBitmap);
			fc_entry->isFinished = false;
		}
	}
	else if (fc_btreeEntry.findItem(&fc_btreeEntry, fc_stackEntry))
	{
		IndexTuple	fc_itup = (IndexTuple) PageGetItem(fc_page, PageGetItemId(fc_page, fc_stackEntry->off));

		if (GinIsPostingTree(fc_itup))
		{
			BlockNumber fc_rootPostingTree = GinGetPostingTree(fc_itup);
			GinBtreeStack *fc_stack;
			Page		fc_page;
			ItemPointerData fc_minItem;

			/*
			 * 这是一个等值扫描，因此锁定发布树的根。它代表了一个对确切键值的锁定，并覆盖了发布树中的所有条目。
			 */
			PredicateLockPage(fc_ginstate->index, fc_rootPostingTree, fc_snapshot);

			/*
			 * 在接触发布树之前，我们应该解锁条目页面，以防止与清理进程发生死锁。因为条目从未从页面中删除，而发布树也从未减少到发布列表，因此在获取发布树根的 BlockNumber 后可以解锁页面。
			 */
			LockBuffer(fc_stackEntry->buffer, GIN_UNLOCK);
			fc_needUnlock = false;

			fc_stack = ginScanBeginPostingTree(&fc_entry->btree, fc_ginstate->index,
											fc_rootPostingTree, fc_snapshot);
			fc_entry->buffer = fc_stack->buffer;

			/*
			 * 我们保持缓冲区固定，因为我们需要防止在扫描过程中删除页面。请参见 GIN 的清理实现。在调用 freeGinBtreeStack() 后，RefCount 会增加，以保持缓冲区固定。
			 */
			IncrBufferRefCount(fc_entry->buffer);

			fc_page = BufferGetPage(fc_entry->buffer);

			/*
			 * 将第一页加载到内存中。
			 */
			ItemPointerSetMin(&fc_minItem);
			fc_entry->list = GinDataLeafPageGetItems(fc_page, &fc_entry->nlist, fc_minItem);

			fc_entry->predictNumberResult = fc_stack->predictNumber * fc_entry->nlist;

			LockBuffer(fc_entry->buffer, GIN_UNLOCK);
			freeGinBtreeStack(fc_stack);
			fc_entry->isFinished = false;
		}
		else
		{
			/*
			 * 锁定条目叶页面。这比必要的粒度更粗，因为它会与任何落在同一叶页面上的插入发生冲突，而不仅仅是我们搜索的确切键。 但是锁定单个元组将要求在因插入或清理而移动时更新该锁，这似乎太复杂了。
			 */
			PredicateLockPage(fc_ginstate->index,
							  BufferGetBlockNumber(fc_stackEntry->buffer),
							  fc_snapshot);
			if (GinGetNPosting(fc_itup) > 0)
			{
				fc_entry->list = ginReadTuple(fc_ginstate, fc_entry->attnum, fc_itup,
										   &fc_entry->nlist);
				fc_entry->predictNumberResult = fc_entry->nlist;

				fc_entry->isFinished = false;
			}
		}
	}
	else
	{
		/*
		 * 没有找到条目。谓词锁定叶页面，以锁定条目本应存在的位置（如果有的话）。
		 */
		PredicateLockPage(fc_ginstate->index,
						  BufferGetBlockNumber(fc_stackEntry->buffer), fc_snapshot);
	}

	if (fc_needUnlock)
		LockBuffer(fc_stackEntry->buffer, GIN_UNLOCK);
	freeGinBtreeStack(fc_stackEntry);
}

/*
 * 扫描条目索引的比较函数。根据 predictNumberResult 排序，最不频繁的条目优先。
 */
static int fc_entryIndexByFrequencyCmp(const void *fc_a1, const void *fc_a2, void *fc_arg)
{
	const GinScanKey fc_key = (const GinScanKey) fc_arg;
	int			fc_i1 = *(const int *) fc_a1;
	int			fc_i2 = *(const int *) fc_a2;
	uint32		fc_n1 = fc_key->scanEntry[fc_i1]->predictNumberResult;
	uint32		fc_n2 = fc_key->scanEntry[fc_i2]->predictNumberResult;

	if (fc_n1 < fc_n2)
		return -1;
	else if (fc_n1 == fc_n2)
		return 0;
	else
		return 1;
}

static void fc_startScanKey(GinState *fc_ginstate, GinScanOpaque fc_so, GinScanKey fc_key)
{
	MemoryContext fc_oldCtx = CurrentMemoryContext;
	int			fc_i;
	int			fc_j;
	int		   *fc_entryIndexes;

	ItemPointerSetMin(&fc_key->curItem);
	fc_key->curItemMatches = false;
	fc_key->recheckCurItem = false;
	fc_key->isFinished = false;

	/*
	 * 将条目分为两个不同的集合：必需和附加。
	 * 附加条目单独不足以匹配，没有任何来自必需集合的项目，
	 * 但一致函数需要它们来决定一个项目是否匹配。
	 * 在扫描时，我们可以跳过没有任何对应匹配的附加条目中的项目
	 * 在任何必需条目中。如果频繁术语可以放入附加集合，
	 * 那么这大大加快了“频繁和稀有”这样的查询。
	 *
	 * 将条目分为这两个集合的方式有很多合法的方法。
	 * 保守的分法是将所有内容放入必需集合，但越多放入附加集合，
	 * 在扫描过程中就可以越多跳过。为了最大化跳过，
	 * 我们尽量将尽可能多的频繁项目放入附加集合，
	 * 将不那么频繁的项目放入必需集合。为此，按频率
	 * （predictNumberResult）对条目进行排序，
	 * 按此顺序将条目放入必需集合，直到一致函数表示剩余
	 * 条目中没有任何可以形成匹配的条目，而没有必需集合中的任何项目。
	 * 剩下的条目进入附加集合。
	 *
	 * 排除仅扫描键已知没有必需条目。
	 */
	if (fc_key->excludeOnly)
	{
		MemoryContextSwitchTo(fc_so->keyCtx);

		fc_key->nrequired = 0;
		fc_key->nadditional = fc_key->nentries;
		fc_key->additionalEntries = palloc(fc_key->nadditional * sizeof(GinScanEntry));
		for (fc_i = 0; fc_i < fc_key->nadditional; fc_i++)
			fc_key->additionalEntries[fc_i] = fc_key->scanEntry[fc_i];
	}
	else if (fc_key->nentries > 1)
	{
		MemoryContextSwitchTo(fc_so->tempCtx);

		fc_entryIndexes = (int *) palloc(sizeof(int) * fc_key->nentries);
		for (fc_i = 0; fc_i < fc_key->nentries; fc_i++)
			fc_entryIndexes[fc_i] = fc_i;
		qsort_arg(fc_entryIndexes, fc_key->nentries, sizeof(int),
				  fc_entryIndexByFrequencyCmp, fc_key);

		for (fc_i = 0; fc_i < fc_key->nentries - 1; fc_i++)
		{
			/* 将所有条目 <= i 赋值为 FALSE，其余赋值为 MAYBE */
			for (fc_j = 0; fc_j <= fc_i; fc_j++)
				fc_key->entryRes[fc_entryIndexes[fc_j]] = GIN_FALSE;
			for (fc_j = fc_i + 1; fc_j < fc_key->nentries; fc_j++)
				fc_key->entryRes[fc_entryIndexes[fc_j]] = GIN_MAYBE;

			if (fc_key->triConsistentFn(fc_key) == GIN_FALSE)
				break;
		}
		/* i 现在是最后一个必需条目。 */

		MemoryContextSwitchTo(fc_so->keyCtx);

		fc_key->nrequired = fc_i + 1;
		fc_key->nadditional = fc_key->nentries - fc_key->nrequired;
		fc_key->requiredEntries = palloc(fc_key->nrequired * sizeof(GinScanEntry));
		fc_key->additionalEntries = palloc(fc_key->nadditional * sizeof(GinScanEntry));

		fc_j = 0;
		for (fc_i = 0; fc_i < fc_key->nrequired; fc_i++)
			fc_key->requiredEntries[fc_i] = fc_key->scanEntry[fc_entryIndexes[fc_j++]];
		for (fc_i = 0; fc_i < fc_key->nadditional; fc_i++)
			fc_key->additionalEntries[fc_i] = fc_key->scanEntry[fc_entryIndexes[fc_j++]];

		/* 在 consistentFn 调用后进行清理（也释放 entryIndexes） */
		MemoryContextReset(fc_so->tempCtx);
	}
	else
	{
		MemoryContextSwitchTo(fc_so->keyCtx);

		fc_key->nrequired = 1;
		fc_key->nadditional = 0;
		fc_key->requiredEntries = palloc(1 * sizeof(GinScanEntry));
		fc_key->requiredEntries[0] = fc_key->scanEntry[0];
	}
	MemoryContextSwitchTo(fc_oldCtx);
}

static void fc_startScan(IndexScanDesc fc_scan)
{
	GinScanOpaque fc_so = (GinScanOpaque) fc_scan->opaque;
	GinState   *fc_ginstate = &fc_so->ginstate;
	uint32		fc_i;

	for (fc_i = 0; fc_i < fc_so->totalentries; fc_i++)
		fc_startScanEntry(fc_ginstate, fc_so->entries[fc_i], fc_scan->xs_snapshot);

	if (GinFuzzySearchLimit > 0)
	{
		/*
		 * 如果所有键都超过阈值，我们将尝试减少结果，我们
		 * 希望（并且仅希望，因为我们对数组交集操作的假设并不成立），
		 * 总结果不会超过最小的 predictNumberResult。
		 */
		bool		fc_reduce = true;

		for (fc_i = 0; fc_i < fc_so->totalentries; fc_i++)
		{
			if (fc_so->entries[fc_i]->predictNumberResult <= fc_so->totalentries * GinFuzzySearchLimit)
			{
				fc_reduce = false;
				break;
			}
		}
		if (fc_reduce)
		{
			for (fc_i = 0; fc_i < fc_so->totalentries; fc_i++)
			{
				fc_so->entries[fc_i]->predictNumberResult /= fc_so->totalentries;
				fc_so->entries[fc_i]->reduceResult = true;
			}
		}
	}

	/*
	 * 现在我们已经有了条目频率的估计，完成
	 * 初始化扫描键。
	 */
	for (fc_i = 0; fc_i < fc_so->nkeys; fc_i++)
		fc_startScanKey(fc_ginstate, fc_so, fc_so->keys + fc_i);
}

/*
 * 从发布树加载下一批项指针。
 *
 * 注意，我们将页面复制到 GinScanEntry->list 数组中并解锁它，
 * 但保持它固定以防止与清理干扰。
 */
static void fc_entryLoadMoreItems(GinState *fc_ginstate, GinScanEntry fc_entry,
				   ItemPointerData fc_advancePast, Snapshot fc_snapshot)
{
	Page		fc_page;
	int			fc_i;
	bool		fc_stepright;

	if (!BufferIsValid(fc_entry->buffer))
	{
		fc_entry->isFinished = true;
		return;
	}

	/*
	 * 我们有两种策略来找到正确的页面：从
	 * 当前页面向右迈进，或从根部再次下降树。
	 * 如果 advancePast 等于当前项，下一个匹配项应该在
	 * 下一页上，所以我们向右迈进。否则，从根部下降。
	 */
	if (ginCompareItemPointers(&fc_entry->curItem, &fc_advancePast) == 0)
	{
		fc_stepright = true;
		LockBuffer(fc_entry->buffer, GIN_SHARE);
	}
	else
	{
		GinBtreeStack *fc_stack;

		ReleaseBuffer(fc_entry->buffer);

		/*
		 * 设置搜索键，并找到正确的叶子页。
		 */
		if (ItemPointerIsLossyPage(&fc_advancePast))
		{
			ItemPointerSet(&fc_entry->btree.itemptr,
						   GinItemPointerGetBlockNumber(&fc_advancePast) + 1,
						   FirstOffsetNumber);
		}
		else
		{
			ItemPointerSet(&fc_entry->btree.itemptr,
						   GinItemPointerGetBlockNumber(&fc_advancePast),
						   OffsetNumberNext(GinItemPointerGetOffsetNumber(&fc_advancePast)));
		}
		fc_entry->btree.fullScan = false;
		fc_stack = ginFindLeafPage(&fc_entry->btree, true, false, fc_snapshot);

		/* 我们不需要堆栈，只需要缓冲区。 */
		fc_entry->buffer = fc_stack->buffer;
		IncrBufferRefCount(fc_entry->buffer);
		freeGinBtreeStack(fc_stack);
		fc_stepright = false;
	}

	elog(DEBUG2, "entryLoadMoreItems, %u/%u, skip: %d",
		 GinItemPointerGetBlockNumber(&fc_advancePast),
		 GinItemPointerGetOffsetNumber(&fc_advancePast),
		 !fc_stepright);

	fc_page = BufferGetPage(fc_entry->buffer);
	for (;;)
	{
		fc_entry->offset = InvalidOffsetNumber;
		if (fc_entry->list)
		{
			pfree(fc_entry->list);
			fc_entry->list = NULL;
			fc_entry->nlist = 0;
		}

		if (fc_stepright)
		{
			/*
			 * 我们已经处理了此页面上的所有条目。如果这是
			 * 树中的最后一页，我们就完成了。
			 */
			if (GinPageRightMost(fc_page))
			{
				UnlockReleaseBuffer(fc_entry->buffer);
				fc_entry->buffer = InvalidBuffer;
				fc_entry->isFinished = true;
				return;
			}

			/*
			 * 步骤到下一页，跟随右链接。然后找到
			 * 大于 advancePast 的第一个 ItemPointer。
			 */
			fc_entry->buffer = ginStepRight(fc_entry->buffer,
										 fc_ginstate->index,
										 GIN_SHARE);
			fc_page = BufferGetPage(fc_entry->buffer);
		}
		fc_stepright = true;

		if (GinPageGetOpaque(fc_page)->flags & GIN_DELETED)
			continue;			/* 页面已被并发清理删除 */

		/*
		 * 第一个项 > advancePast 可能不在此页面上，而是在右侧某处，如果页面被拆分，或者来自查询中另一个键的非匹配项使我们跳过了某些条目。继续跟踪右侧链接，直到我们重新找到正确的页面。
		 */
		if (!GinPageRightMost(fc_page) &&
			ginCompareItemPointers(&fc_advancePast, GinDataPageGetRightBound(fc_page)) >= 0)
		{
			/*
			 * 我们正在寻找的项 > 页面的右边界，因此不能在此页面上。
			 */
			continue;
		}

		fc_entry->list = GinDataLeafPageGetItems(fc_page, &fc_entry->nlist, fc_advancePast);

		for (fc_i = 0; fc_i < fc_entry->nlist; fc_i++)
		{
			if (ginCompareItemPointers(&fc_advancePast, &fc_entry->list[fc_i]) < 0)
			{
				fc_entry->offset = fc_i;

				if (GinPageRightMost(fc_page))
				{
					/* 在处理完复制的项后，我们完成了。 */
					UnlockReleaseBuffer(fc_entry->buffer);
					fc_entry->buffer = InvalidBuffer;
				}
				else
					LockBuffer(fc_entry->buffer, GIN_UNLOCK);
				return;
			}
		}
	}
}

#define gin_rand() pg_prng_double(&pg_global_prng_state)
#define dropItem(e) ( gin_rand() > ((double)GinFuzzySearchLimit)/((double)((e)->predictNumberResult)) )

/*
 * 将 entry->curItem 设置为下一个堆项指针 > advancePast，适用于一个扫描键的一个条目，或者如果没有更多项，则将 entry->isFinished 设置为 true。
 *
 * 项指针以升序返回。
 *
 * 注意：这可以返回一个“损失页面”项指针，表示该条目可能与该堆页面上的所有项匹配。然而，不允许为同一页面同时返回一个损失页面指针和准确（常规）项指针。 （这样做会破坏 keyGetItem 和 scanGetItem 中的键组合逻辑；请参见 scanGetItem 中的注释。）在当前实现中，这是通过 tidbitmaps 的行为来保证的。
 */
static void fc_entryGetItem(GinState *fc_ginstate, GinScanEntry fc_entry,
			 ItemPointerData fc_advancePast, Snapshot fc_snapshot)
{
	Assert(!fc_entry->isFinished);

	Assert(!ItemPointerIsValid(&fc_entry->curItem) ||
		   ginCompareItemPointers(&fc_entry->curItem, &fc_advancePast) <= 0);

	if (fc_entry->matchBitmap)
	{
		/* 位图结果 */
		BlockNumber fc_advancePastBlk = GinItemPointerGetBlockNumber(&fc_advancePast);
		OffsetNumber fc_advancePastOff = GinItemPointerGetOffsetNumber(&fc_advancePast);

		for (;;)
		{
			/*
			 * 如果我们已经耗尽了此块上的所有项，则移动到位图中的下一个块。
			 */
			while (fc_entry->matchResult == NULL ||
				   (fc_entry->matchResult->ntuples >= 0 &&
					fc_entry->offset >= fc_entry->matchResult->ntuples) ||
				   fc_entry->matchResult->blockno < fc_advancePastBlk ||
				   (ItemPointerIsLossyPage(&fc_advancePast) &&
					fc_entry->matchResult->blockno == fc_advancePastBlk))
			{
				fc_entry->matchResult = tbm_iterate(fc_entry->matchIterator);

				if (fc_entry->matchResult == NULL)
				{
					ItemPointerSetInvalid(&fc_entry->curItem);
					tbm_end_iterate(fc_entry->matchIterator);
					fc_entry->matchIterator = NULL;
					fc_entry->isFinished = true;
					break;
				}

				/*
				 * 将计数器重置为 entry->matchResult 的开头。注意：如果 matchResult 是损失的，entry->offset 仍然大于 matchResult->ntuples。因此，在下一个调用中，我们将从 TIDBitmap 获取下一个结果。
				 */
				fc_entry->offset = 0;
			}
			if (fc_entry->isFinished)
				break;

			/*
			 * 我们现在处于 advancePast 之后的第一个页面上，该页面上有任何项。如果这是一个损失结果，则返回它。
			 */
			if (fc_entry->matchResult->ntuples < 0)
			{
				ItemPointerSetLossyPage(&fc_entry->curItem,
										fc_entry->matchResult->blockno);

				/*
				 * 我们不妨退出循环；即使启用了正确的结果减少，我们也无法估计此页面上的结果数量来支持正确的减少。
				 */
				break;
			}

			/*
			 * 不是一个损失页面。跳过任何偏移量 <= advancePast，并返回该值。
			 */
			if (fc_entry->matchResult->blockno == fc_advancePastBlk)
			{
				/*
				 * 首先，对页面上的最后偏移量进行快速检查。如果该值 > advancePast，则所有其他偏移量也如此，因此只需返回顶部以获取下一页面。
				 */
				if (fc_entry->matchResult->offsets[fc_entry->matchResult->ntuples - 1] <= fc_advancePastOff)
				{
					fc_entry->offset = fc_entry->matchResult->ntuples;
					continue;
				}

				/* 否则扫描以找到第一个项 > advancePast */
				while (fc_entry->matchResult->offsets[fc_entry->offset] <= fc_advancePastOff)
					fc_entry->offset++;
			}

			ItemPointerSet(&fc_entry->curItem,
						   fc_entry->matchResult->blockno,
						   fc_entry->matchResult->offsets[fc_entry->offset]);
			fc_entry->offset++;

			/* 除非我们需要减少结果，否则完成 */
			if (!fc_entry->reduceResult || !dropItem(fc_entry))
				break;
		}
	}
	else if (!BufferIsValid(fc_entry->buffer))
	{
		/*
		 * 来自条目元组的发布列表，或发布树的最后一页。
		 */
		for (;;)
		{
			if (fc_entry->offset >= fc_entry->nlist)
			{
				ItemPointerSetInvalid(&fc_entry->curItem);
				fc_entry->isFinished = true;
				break;
			}

			fc_entry->curItem = fc_entry->list[fc_entry->offset++];

			/* 如果我们尚未超过 advancePast，继续扫描 */
			if (ginCompareItemPointers(&fc_entry->curItem, &fc_advancePast) <= 0)
				continue;

			/* 除非我们需要减少结果，否则完成 */
			if (!fc_entry->reduceResult || !dropItem(fc_entry))
				break;
		}
	}
	else
	{
		/* 发布树 */
		for (;;)
		{
			/* 如果我们已处理当前批次，则加载更多项 */
			while (fc_entry->offset >= fc_entry->nlist)
			{
				fc_entryLoadMoreItems(fc_ginstate, fc_entry, fc_advancePast, fc_snapshot);

				if (fc_entry->isFinished)
				{
					ItemPointerSetInvalid(&fc_entry->curItem);
					return;
				}
			}

			fc_entry->curItem = fc_entry->list[fc_entry->offset++];

			/* 如果我们尚未超过 advancePast，继续扫描 */
			if (ginCompareItemPointers(&fc_entry->curItem, &fc_advancePast) <= 0)
				continue;

			/* 除非我们需要减少结果，否则完成 */
			if (!fc_entry->reduceResult || !dropItem(fc_entry))
				break;

			/*
			 * 提前 advancePast（以便 entryLoadMoreItems 将加载正确的数据），并继续扫描
			 */
			fc_advancePast = fc_entry->curItem;
		}
	}
}

/*
 * 确定在该扫描键的输入条目流中 "当前" 项，其值大于 advancePast，并测试它是否满足扫描键的 qual 条件。
 *
 * 当前项是输入中最小的 curItem。key->curItem 被设置为该值。key->curItemMatches 被设置为指示该 TID 是否通过了 consistentFn 测试。如果是，key->recheckCurItem 被设为 true，仅当该项指针需要复查时（包括项指针是丢失页指针的情况）。
 *
 * 如果所有条目流都耗尽，将 key->isFinished 设置为 true。
 *
 * 项指针必须按升序返回。
 *
 * 注意：这可以返回一个 "丢失页" 项指针，表示该键可能与该堆页上的所有项匹配。然而，不允许对同一页返回丢失页指针和精确（常规）项指针。(这样做会破坏 scanGetItem 中的键组合逻辑。)
 */
static void fc_keyGetItem(GinState *fc_ginstate, MemoryContext fc_tempCtx, GinScanKey fc_key,
		   ItemPointerData fc_advancePast, Snapshot fc_snapshot)
{
	ItemPointerData fc_minItem;
	ItemPointerData fc_curPageLossy;
	uint32		fc_i;
	bool		fc_haveLossyEntry;
	GinScanEntry fc_entry;
	GinTernaryValue fc_res;
	MemoryContext fc_oldCtx;
	bool		fc_allFinished;

	Assert(!fc_key->isFinished);

	/*
	 * 我们可能已经测试过这个项；如果是这样，没必要重复工作。
	 * （注意：“>” 的情况可能发生，如果 advancePast 是精确的，但我们
	 * 之前不得不将 curItem 设置为丢失页指针。）
	 */
	if (ginCompareItemPointers(&fc_key->curItem, &fc_advancePast) > 0)
		return;

	/*
	 * 找到活跃条目流中大于 advancePast 的最小项。
	 *
	 * 注意：丢失页条目是通过一个最大值为偏移量（0xffff）的 ItemPointer 编码的，
	 * 因此它会在同一页的任何精确条目之后排序。所以我们会更倾向于返回精确的指针，而不是丢失的指针，这很好。
	 */
	ItemPointerSetMax(&fc_minItem);
	fc_allFinished = true;
	for (fc_i = 0; fc_i < fc_key->nrequired; fc_i++)
	{
		fc_entry = fc_key->requiredEntries[fc_i];

		if (fc_entry->isFinished)
			continue;

		/*
		 * 如果有必要，提前这个流。
		 *
		 * 特别是，由于 entry->curItem 已通过 ItemPointerSetMin 初始化，
		 * 这确保我们在第一次调用时获取每个条目的第一个项。
		 */
		if (ginCompareItemPointers(&fc_entry->curItem, &fc_advancePast) <= 0)
		{
			fc_entryGetItem(fc_ginstate, fc_entry, fc_advancePast, fc_snapshot);
			if (fc_entry->isFinished)
				continue;
		}

		fc_allFinished = false;
		if (ginCompareItemPointers(&fc_entry->curItem, &fc_minItem) < 0)
			fc_minItem = fc_entry->curItem;
	}

	if (fc_allFinished && !fc_key->excludeOnly)
	{
		/* 所有条目均已完成 */
		fc_key->isFinished = true;
		return;
	}

	if (!fc_key->excludeOnly)
	{
		/*
		 * 对于正常的扫描键，我们现在知道没有匹配项 < minItem。
		 *
		 * 如果 minItem 是丢失的，这意味着在 requiredEntries 中该页上没有
		 * 精确的项，因为丢失的指针在精确项之后排序。然而，可能在 additionalEntries 中
		 * 仍然有同一页的精确项，因此我们不能超过它们。
		 */
		if (ItemPointerIsLossyPage(&fc_minItem))
		{
			if (GinItemPointerGetBlockNumber(&fc_advancePast) <
				GinItemPointerGetBlockNumber(&fc_minItem))
			{
				ItemPointerSet(&fc_advancePast,
							   GinItemPointerGetBlockNumber(&fc_minItem),
							   InvalidOffsetNumber);
			}
		}
		else
		{
			Assert(GinItemPointerGetOffsetNumber(&fc_minItem) > 0);
			ItemPointerSet(&fc_advancePast,
						   GinItemPointerGetBlockNumber(&fc_minItem),
						   OffsetNumberPrev(GinItemPointerGetOffsetNumber(&fc_minItem)));
		}
	}
	else
	{
		/*
		 * excludeOnly 扫描键没有任何项是必然出现在匹配项中的。
		 * 因此，我们考虑紧随 advancePast 之后的项。
		 */
		Assert(fc_key->nrequired == 0);
		ItemPointerSet(&fc_minItem,
					   GinItemPointerGetBlockNumber(&fc_advancePast),
					   OffsetNumberNext(GinItemPointerGetOffsetNumber(&fc_advancePast)));
	}

	/*
	 * 我们可能还没有为这个 TID 加载所有的条目流。
	 * 我们可以调用 consistent 函数，传递 MAYBE 给那些条目，看看它是否
	 * 可以根据我们拥有的信息判断这个 TID 是否匹配。但是，如果 consistent 函数很昂贵，并且实际上无法
	 * 使用部分信息决定，那么可能会造成很大的损失。因此，在调用 consistent 函数之前，加载所有
	 * 附加条目。
	 */
	for (fc_i = 0; fc_i < fc_key->nadditional; fc_i++)
	{
		fc_entry = fc_key->additionalEntries[fc_i];

		if (fc_entry->isFinished)
			continue;

		if (ginCompareItemPointers(&fc_entry->curItem, &fc_advancePast) <= 0)
		{
			fc_entryGetItem(fc_ginstate, fc_entry, fc_advancePast, fc_snapshot);
			if (fc_entry->isFinished)
				continue;
		}

		/*
		 * 通常，additionalEntries 中的项不能有 curItem 大于 minItem。但如果 minItem 是一个损失页面，那么 additionalEntries 中可能会有相同页面上的确切项。
		 */
		if (ginCompareItemPointers(&fc_entry->curItem, &fc_minItem) < 0)
		{
			Assert(ItemPointerIsLossyPage(&fc_minItem));
			fc_minItem = fc_entry->curItem;
		}
	}

	/*
	 * 好的，我们现在已将所有条目推进到 minItem。设置 key->curItem，并执行 consistentFn 测试。
	 *
	 * 损失页面条目带来了问题，因为我们不知道要传递给 consistentFn 的正确 entryRes 状态，也不知道它将采用什么组合逻辑（可能是 AND、OR 或甚至 NOT）。如果逻辑是 OR，那么即使其他条目都不匹配，consistentFn 也可能对损失页面中的所有项成功。
	 *
	 * 我们的策略是调用三态一致性函数，将损失页面条目设置为 MAYBE，所有其他条目设置为 FALSE。如果返回 FALSE，则损失项单独无法匹配，因此我们不需要返回损失页面指针。否则，返回损失页面指针以指示必须检查整个堆页面。（在后续调用中，我们将不执行任何操作，直到 minItem 完全超出页面，从而确保我们不会为同一页面返回常规和损失指针。）
	 *
	 * 一个例外是，在“隐藏”条目中传递损失指针的内容并不重要，因为 consistentFn 的结果不能依赖于它们。我们也可以将它们作为 MAYBE 传递，但如果我们使用的是三态一致性函数的“垫片”实现（参见 ginlogic.c），最好尽可能少地传递 MAYBE。因此将它们作为 true 传递。
	 *
	 * 请注意，只有指向当前项页面的损失页面条目应该触发此处理；我们可能在条目数组中有未来的损失页面，但它们尚不相关。
	 */
	fc_key->curItem = fc_minItem;
	ItemPointerSetLossyPage(&fc_curPageLossy,
							GinItemPointerGetBlockNumber(&fc_key->curItem));
	fc_haveLossyEntry = false;
	for (fc_i = 0; fc_i < fc_key->nentries; fc_i++)
	{
		fc_entry = fc_key->scanEntry[fc_i];
		if (fc_entry->isFinished == false &&
			ginCompareItemPointers(&fc_entry->curItem, &fc_curPageLossy) == 0)
		{
			if (fc_i < fc_key->nuserentries)
				fc_key->entryRes[fc_i] = GIN_MAYBE;
			else
				fc_key->entryRes[fc_i] = GIN_TRUE;
			fc_haveLossyEntry = true;
		}
		else
			fc_key->entryRes[fc_i] = GIN_FALSE;
	}

	/* 准备在临时上下文中调用 consistentFn */
	fc_oldCtx = MemoryContextSwitchTo(fc_tempCtx);

	if (fc_haveLossyEntry)
	{
		/* 有损失页面条目，因此检查整个页面是否匹配 */
		fc_res = fc_key->triConsistentFn(fc_key);

		if (fc_res == GIN_TRUE || fc_res == GIN_MAYBE)
		{
			/* 是的，清理... */
			MemoryContextSwitchTo(fc_oldCtx);
			MemoryContextReset(fc_tempCtx);

			/* 并返回整个页面的损失指针 */
			fc_key->curItem = fc_curPageLossy;
			fc_key->curItemMatches = true;
			fc_key->recheckCurItem = true;
			return;
		}
	}

	/*
	 * 此时我们知道不需要返回损失整个页面指针，但我们可能有单个确切项指针的匹配，可能与损失指针组合。在三元一致性函数中将损失指针作为 MAYBE 传递，让它决定这个元组是否满足整体键，即使我们不知道损失条目是否匹配。
	 *
	 * 准备要传递给 consistentFn 的 entryRes 数组。
	 */
	for (fc_i = 0; fc_i < fc_key->nentries; fc_i++)
	{
		fc_entry = fc_key->scanEntry[fc_i];
		if (fc_entry->isFinished)
			fc_key->entryRes[fc_i] = GIN_FALSE;
#if 0

		/*
		 * 这种情况目前无法发生，因为我们之前已加载此项的所有条目。
		 */
		else if (ginCompareItemPointers(&entry->curItem, &advancePast) <= 0)
			key->entryRes[i] = GIN_MAYBE;
#endif
		else if (ginCompareItemPointers(&fc_entry->curItem, &fc_curPageLossy) == 0)
			fc_key->entryRes[fc_i] = GIN_MAYBE;
		else if (ginCompareItemPointers(&fc_entry->curItem, &fc_minItem) == 0)
			fc_key->entryRes[fc_i] = GIN_TRUE;
		else
			fc_key->entryRes[fc_i] = GIN_FALSE;
	}

	fc_res = fc_key->triConsistentFn(fc_key);

	switch (fc_res)
	{
		case GIN_TRUE:
			fc_key->curItemMatches = true;
			/* triConsistentFn 设置 recheckCurItem */
			break;

		case GIN_FALSE:
			fc_key->curItemMatches = false;
			break;

		case GIN_MAYBE:
			fc_key->curItemMatches = true;
			fc_key->recheckCurItem = true;
			break;

		default:

			/*
			 * 'default' 情况不应该发生，但如果一致性函数返回一些错误值，这是安全结果
			 */
			fc_key->curItemMatches = true;
			fc_key->recheckCurItem = true;
			break;
	}

	/*
	 * 我们有一个元组，我们知道它是否匹配。如果它不匹配，
	 * 我们可以继续寻找下一个匹配的元组，但我们先退出
	 * 并给 scanGetItem 机会来推进其他键。它们可能能够
	 * 跳过到更高的 TID，从而让我们节省工作。
	 */

	/* 在 consistentFn 调用后进行清理 */
	MemoryContextSwitchTo(fc_oldCtx);
	MemoryContextReset(fc_tempCtx);
}

/*
 * 从扫描中获取下一个堆项指针（在 advancePast 之后）。
 * 如果找到任何东西，则返回 true。
 * 成功时，*item 和 *recheck 被设置。
 *
 * 注意：这几乎与 keyGetItem() 中的逻辑相同，除了我们
 * 知道键将与 AND 逻辑结合，而在 keyGetItem() 中
 * 组合逻辑仅对 consistentFn 可知。
 */
static bool fc_scanGetItem(IndexScanDesc fc_scan, ItemPointerData fc_advancePast,
			ItemPointerData *fc_item, bool *fc_recheck)
{
	GinScanOpaque fc_so = (GinScanOpaque) fc_scan->opaque;
	uint32		fc_i;
	bool		fc_match;

	/*----------
	 * 同步推进扫描键，直到我们找到一个匹配
	 * 所有键的项。如果任何键报告 isFinished，意味着它的
	 * 条目子集已耗尽，我们可以停止。否则，将 *item 设置为下一个
	 * 匹配的项。
	 *
	 * 这个逻辑只有在 keyGetItem 流不能包含相同页面的
	 * 精确和损失指针的情况下才有效。否则，我们可能会遇到
	 * 这样的案例
	 *
	 *		stream 1		stream 2
	 *		...             ...
	 *		42/6			42/7
	 *		50/1			42/0xffff
	 *		...             ...
	 *
	 * 我们会得出结论，42/6不是匹配，并推进流1，
	 * 因此永远不会检测到流2中的损失指针的匹配。
	 * (keyGetItem 对比 entryGetItem 有类似的问题。)
	 *----------
	 */
	do
	{
		ItemPointerSetMin(fc_item);
		fc_match = true;
		for (fc_i = 0; fc_i < fc_so->nkeys && fc_match; fc_i++)
		{
			GinScanKey	fc_key = fc_so->keys + fc_i;

			/*
			 * 如果我们在考虑一个损失页面，跳过 excludeOnly 键。它们
			 * 无法排除整个页面。
			 */
			if (ItemPointerIsLossyPage(fc_item) && fc_key->excludeOnly)
			{
				/*
				 * ginNewScanKey() 绝不应该将第一个键标记为
				 * excludeOnly。
				 */
				Assert(fc_i > 0);
				continue;
			}

			/* 获取此键的下一个项，其值 > advancePast。 */
			fc_keyGetItem(&fc_so->ginstate, fc_so->tempCtx, fc_key, fc_advancePast,
					   fc_scan->xs_snapshot);

			if (fc_key->isFinished)
				return false;

			/*
			 * 如果没有匹配，我们可以立即得出结论，
			 * 对应于该项的较小项均没有匹配，而无需检查其余的键。
			 */
			if (!fc_key->curItemMatches)
			{
				fc_advancePast = fc_key->curItem;
				fc_match = false;
				break;
			}

			/*
			 * 匹配成功。我们可以得出结论，没有小于匹配的项，因此
			 * 其他键流可以跳到该项。
			 *
			 * 不过要注意损失指针；从损失指针中，我们只能得出
			 * 没有小于此 *块* 的匹配。
			 */
			if (ItemPointerIsLossyPage(&fc_key->curItem))
			{
				if (GinItemPointerGetBlockNumber(&fc_advancePast) <
					GinItemPointerGetBlockNumber(&fc_key->curItem))
				{
					ItemPointerSet(&fc_advancePast,
								   GinItemPointerGetBlockNumber(&fc_key->curItem),
								   InvalidOffsetNumber);
				}
			}
			else
			{
				Assert(GinItemPointerGetOffsetNumber(&fc_key->curItem) > 0);
				ItemPointerSet(&fc_advancePast,
							   GinItemPointerGetBlockNumber(&fc_key->curItem),
							   OffsetNumberPrev(GinItemPointerGetOffsetNumber(&fc_key->curItem)));
			}

			/*
			 * 如果这是第一个键，请将此位置记为潜在
			 * 匹配，并继续检查其余键。
			 *
			 * 否则，检查这是否是我们为之前的键检查的
			 * 相同项（或相同页面的损失指针）。如果
			 * 不是，则返回检查之前的键针对该项（我们
			 * 也会再次检查此键，但 keyGetItem 返回很快
			 * 为此）。
			 */
			if (fc_i == 0)
			{
				*fc_item = fc_key->curItem;
			}
			else
			{
				if (ItemPointerIsLossyPage(&fc_key->curItem) ||
					ItemPointerIsLossyPage(fc_item))
				{
					Assert(GinItemPointerGetBlockNumber(&fc_key->curItem) >= GinItemPointerGetBlockNumber(fc_item));
					fc_match = (GinItemPointerGetBlockNumber(&fc_key->curItem) ==
							 GinItemPointerGetBlockNumber(fc_item));
				}
				else
				{
					Assert(ginCompareItemPointers(&fc_key->curItem, fc_item) >= 0);
					fc_match = (ginCompareItemPointers(&fc_key->curItem, fc_item) == 0);
				}
			}
		}
	} while (!fc_match);

	Assert(!ItemPointerIsMin(fc_item));

	/*
	 * 现在 *item 包含在满足所有键的前一个结果之后的第一个
	 * ItemPointer，或者是对同一页面的损失引用。
	 *
	 * 如果任何键被标记为重新检查，则必须返回 recheck = true。
	 */
	*fc_recheck = false;
	for (fc_i = 0; fc_i < fc_so->nkeys; fc_i++)
	{
		GinScanKey	fc_key = fc_so->keys + fc_i;

		if (fc_key->recheckCurItem)
		{
			*fc_recheck = true;
			break;
		}
	}

	return true;
}


/*
 * 扫描待处理列表的函数
 */


/*
 * 获取待处理列表中下一个需要检查的堆行的 ItemPointer。
 * 如果没有更多项，则返回 false。在包含多个堆行的页面上，
 * 它分别返回每一行，在包含部分堆行的页面上返回每页数据。
 * pos->firstOffset 和 pos->lastOffset 被设置以识别
 * 归属于该堆行的待处理列表元组的范围。
 *
 * 在进入时假定 pendingBuffer 被锁定并共享锁定，
 * 成功退出时被锁定并共享锁定。在失败退出时会释放。
 */
static bool fc_scanGetCandidate(IndexScanDesc fc_scan, pendingPosition *fc_pos)
{
	OffsetNumber fc_maxoff;
	Page		fc_page;
	IndexTuple	fc_itup;

	ItemPointerSetInvalid(&fc_pos->item);
	for (;;)
	{
		fc_page = BufferGetPage(fc_pos->pendingBuffer);
		TestForOldSnapshot(fc_scan->xs_snapshot, fc_scan->indexRelation, fc_page);

		fc_maxoff = PageGetMaxOffsetNumber(fc_page);
		if (fc_pos->firstOffset > fc_maxoff)
		{
			BlockNumber fc_blkno = GinPageGetOpaque(fc_page)->rightlink;

			if (fc_blkno == InvalidBlockNumber)
			{
				UnlockReleaseBuffer(fc_pos->pendingBuffer);
				fc_pos->pendingBuffer = InvalidBuffer;

				return false;
			}
			else
			{
				/*
				 * 在插入清理过程中，我们必须防止下一个页面的删除，
				 * 该过程可能试图对当前页面获得独占锁。
				 * 因此，我们在释放当前页面之前锁定下一个页面。
				 */
				Buffer		fc_tmpbuf = ReadBuffer(fc_scan->indexRelation, fc_blkno);

				LockBuffer(fc_tmpbuf, GIN_SHARE);
				UnlockReleaseBuffer(fc_pos->pendingBuffer);

				fc_pos->pendingBuffer = fc_tmpbuf;
				fc_pos->firstOffset = FirstOffsetNumber;
			}
		}
		else
		{
			fc_itup = (IndexTuple) PageGetItem(fc_page, PageGetItemId(fc_page, fc_pos->firstOffset));
			fc_pos->item = fc_itup->t_tid;
			if (GinPageHasFullRow(fc_page))
			{
				/*
				 * 找到下一个行的 itempointer
				 */
				for (fc_pos->lastOffset = fc_pos->firstOffset + 1; fc_pos->lastOffset <= fc_maxoff; fc_pos->lastOffset++)
				{
					fc_itup = (IndexTuple) PageGetItem(fc_page, PageGetItemId(fc_page, fc_pos->lastOffset));
					if (!ItemPointerEquals(&fc_pos->item, &fc_itup->t_tid))
						break;
				}
			}
			else
			{
				/*
				 * 此页面上的所有 itempointers 是相同的
				 */
				fc_pos->lastOffset = fc_maxoff + 1;
			}

			/*
			 * 现在 pos->firstOffset 指向当前堆行的第一个元组，
			 * pos->lastOffset 指向下一个堆行的第一个元组
			 * （或指向页面的末尾）。
			 */
			break;
		}
	}

	return true;
}

/*
 * 从当前元组（off）开始扫描待处理列表页面，直到遇到以下第一个：
 * - 找到匹配项（则返回 true）
 * - 不可能有后续匹配
 * - 元组的属性编号不等于条目的 attrnum
 * - 到达页面末尾
 *
 * datum[]/category[]/datumExtracted[] 数组用于缓存当前页面上
 * gintuple_get_key() 的结果。
 */
static bool fc_matchPartialInPendingList(GinState *fc_ginstate, Page fc_page,
						  OffsetNumber fc_off, OffsetNumber fc_maxoff,
						  GinScanEntry fc_entry,
						  Datum *fc_datum, GinNullCategory *fc_category,
						  bool *fc_datumExtracted)
{
	IndexTuple	fc_itup;
	int32		fc_cmp;

	/* 部分匹配 NULL 是不可能的 */
	if (fc_entry->queryCategory != GIN_CAT_NORM_KEY)
		return false;

	while (fc_off < fc_maxoff)
	{
		fc_itup = (IndexTuple) PageGetItem(fc_page, PageGetItemId(fc_page, fc_off));

		if (gintuple_get_attrnum(fc_ginstate, fc_itup) != fc_entry->attnum)
			return false;

		if (fc_datumExtracted[fc_off - 1] == false)
		{
			fc_datum[fc_off - 1] = gintuple_get_key(fc_ginstate, fc_itup,
											  &fc_category[fc_off - 1]);
			fc_datumExtracted[fc_off - 1] = true;
		}

		/* 一旦遇到 NULL，就不可能有进一步的匹配 */
		if (fc_category[fc_off - 1] != GIN_CAT_NORM_KEY)
			return false;

		/*----------
		 * 检查部分匹配。
		 * 情况 cmp == 0 => 匹配
		 * 情况 cmp > 0 => 不匹配并结束扫描（不可能有后续匹配）
		 * 情况 cmp < 0 => 不匹配并继续扫描
		 *----------
		 */
		fc_cmp = DatumGetInt32(FunctionCall4Coll(&fc_ginstate->comparePartialFn[fc_entry->attnum - 1],
											  fc_ginstate->supportCollation[fc_entry->attnum - 1],
											  fc_entry->queryKey,
											  fc_datum[fc_off - 1],
											  UInt16GetDatum(fc_entry->strategy),
											  PointerGetDatum(fc_entry->extra_data)));
		if (fc_cmp == 0)
			return true;
		else if (fc_cmp > 0)
			return false;

		fc_off++;
	}

	return false;
}

/*
 * 通过查看待处理列表中当前堆行的每个条目为每个键设置
 * entryRes 数组。
 *
 * 如果每个扫描键至少有一个 entryRes 匹配，则返回 true。
 * 这对应于正常索引搜索尝试应用键的 consistentFn 的情况。
 * （不符合该要求的元组不能被正常搜索返回，因为没有条目流会
 * 来源于其 TID。）
 *
 * 在进入时假定 pendingBuffer 被锁定并共享锁定。
 */
static bool fc_collectMatchesForHeapRow(IndexScanDesc fc_scan, pendingPosition *fc_pos)
{
	GinScanOpaque fc_so = (GinScanOpaque) fc_scan->opaque;
	OffsetNumber fc_attrnum;
	Page		fc_page;
	IndexTuple	fc_itup;
	int			fc_i,
				fc_j;

	/*
	 * 重置所有 entryRes 和 hasMatchKey 标志
	 */
	for (fc_i = 0; fc_i < fc_so->nkeys; fc_i++)
	{
		GinScanKey	fc_key = fc_so->keys + fc_i;

		memset(fc_key->entryRes, GIN_FALSE, fc_key->nentries);
	}
	memset(fc_pos->hasMatchKey, false, fc_so->nkeys);

	/*
	 * 外层循环在单个堆行的条目跨越多个待处理列表页面时进行迭代。
	 */
	for (;;)
	{
		Datum		fc_datum[BLCKSZ / sizeof(IndexTupleData)];
		GinNullCategory fc_category[BLCKSZ / sizeof(IndexTupleData)];
		bool		fc_datumExtracted[BLCKSZ / sizeof(IndexTupleData)];

		Assert(fc_pos->lastOffset > fc_pos->firstOffset);
		memset(fc_datumExtracted + fc_pos->firstOffset - 1, 0,
			   sizeof(bool) * (fc_pos->lastOffset - fc_pos->firstOffset));

		fc_page = BufferGetPage(fc_pos->pendingBuffer);
		TestForOldSnapshot(fc_scan->xs_snapshot, fc_scan->indexRelation, fc_page);

		for (fc_i = 0; fc_i < fc_so->nkeys; fc_i++)
		{
			GinScanKey	fc_key = fc_so->keys + fc_i;

			for (fc_j = 0; fc_j < fc_key->nentries; fc_j++)
			{
				GinScanEntry fc_entry = fc_key->scanEntry[fc_j];
				OffsetNumber fc_StopLow = fc_pos->firstOffset,
							fc_StopHigh = fc_pos->lastOffset,
							fc_StopMiddle;

				/* 如果在早先的页面上已经匹配，则无需额外工作 */
				if (fc_key->entryRes[fc_j])
					continue;

				/*
				 * 有趣的元组范围是从 pos->firstOffset 到
				 * pos->lastOffset，按 (attnum, Datum) 排序，如同
				 * 在条目树中进行的那样。因此，我们可以使用二分查找
				 * 来避免线性扫描。
				 */
				while (fc_StopLow < fc_StopHigh)
				{
					int			fc_res;

					fc_StopMiddle = fc_StopLow + ((fc_StopHigh - fc_StopLow) >> 1);

					fc_itup = (IndexTuple) PageGetItem(fc_page, PageGetItemId(fc_page, fc_StopMiddle));

					fc_attrnum = gintuple_get_attrnum(&fc_so->ginstate, fc_itup);

					if (fc_key->attnum < fc_attrnum)
					{
						fc_StopHigh = fc_StopMiddle;
						continue;
					}
					if (fc_key->attnum > fc_attrnum)
					{
						fc_StopLow = fc_StopMiddle + 1;
						continue;
					}

					if (fc_datumExtracted[fc_StopMiddle - 1] == false)
					{
						fc_datum[fc_StopMiddle - 1] =
							gintuple_get_key(&fc_so->ginstate, fc_itup,
											 &fc_category[fc_StopMiddle - 1]);
						fc_datumExtracted[fc_StopMiddle - 1] = true;
					}

					if (fc_entry->queryCategory == GIN_CAT_EMPTY_QUERY)
					{
						/* 根据 searchMode 有特殊行为 */
						if (fc_entry->searchMode == GIN_SEARCH_MODE_ALL)
						{
							/* 匹配任何非 NULL_ITEM */
							if (fc_category[fc_StopMiddle - 1] == GIN_CAT_NULL_ITEM)
								fc_res = -1;
							else
								fc_res = 0;
						}
						else
						{
							/* 匹配所有内容 */
							fc_res = 0;
						}
					}
					else
					{
						fc_res = ginCompareEntries(&fc_so->ginstate,
												fc_entry->attnum,
												fc_entry->queryKey,
												fc_entry->queryCategory,
												fc_datum[fc_StopMiddle - 1],
												fc_category[fc_StopMiddle - 1]);
					}

					if (fc_res == 0)
					{
						/*
						 * 找到精确匹配（在EMPTY_QUERY模式下可能只会有一个）。
						 *
						 * 如果进行部分匹配，从这里向前扫描到页面末尾以检查匹配项。
						 *
						 * 请参见上面的关于元组顺序的注释。
						 */
						if (fc_entry->isPartialMatch)
							fc_key->entryRes[fc_j] =
								fc_matchPartialInPendingList(&fc_so->ginstate,
														  fc_page,
														  fc_StopMiddle,
														  fc_pos->lastOffset,
														  fc_entry,
														  fc_datum,
														  fc_category,
														  fc_datumExtracted);
						else
							fc_key->entryRes[fc_j] = true;

						/* 二分搜索完成 */
						break;
					}
					else if (fc_res < 0)
						fc_StopHigh = fc_StopMiddle;
					else
						fc_StopLow = fc_StopMiddle + 1;
				}

				if (fc_StopLow >= fc_StopHigh && fc_entry->isPartialMatch)
				{
					/*
					 * 此页面上没有精确匹配。如果进行部分匹配，
					 * 从第一个大于目标值的元组开始扫描到页面末尾。请注意，由于我们不记得
					 * comparePartialFn是否在前一页上早期停止，我们将无用地在每个后续页面的第一个元组上应用comparePartialFn。
					 */
					fc_key->entryRes[fc_j] =
						fc_matchPartialInPendingList(&fc_so->ginstate,
												  fc_page,
												  fc_StopHigh,
												  fc_pos->lastOffset,
												  fc_entry,
												  fc_datum,
												  fc_category,
												  fc_datumExtracted);
				}

				fc_pos->hasMatchKey[fc_i] |= fc_key->entryRes[fc_j];
			}
		}

		/* 在扫描的元组上推进firstOffset */
		fc_pos->firstOffset = fc_pos->lastOffset;

		if (GinPageHasFullRow(fc_page))
		{
			/*
			 * 我们已经检查了当前堆行的所有待处理条目。
			 * 退出页面循环。
			 */
			break;
		}
		else
		{
			/*
			 * 前进到当前堆行的待处理条目的下一页。如果没有，报告问题。
			 */
			ItemPointerData fc_item = fc_pos->item;

			if (fc_scanGetCandidate(fc_scan, fc_pos) == false ||
				!ItemPointerEquals(&fc_pos->item, &fc_item))
				elog(ERROR, "could not find additional pending pages for same heap tuple");
		}
	}

	/*
	 * 所有扫描键（除了excludeOnly）都需要至少一个条目匹配。
	 * excludeOnly键是个例外，因为它们隐含的
	 * GIN_CAT_EMPTY_QUERY scanEntry总是匹配。因此，如果所有
	 * 非excludeOnly扫描键至少有一个匹配，则返回"true"。
	 */
	for (fc_i = 0; fc_i < fc_so->nkeys; fc_i++)
	{
		if (fc_pos->hasMatchKey[fc_i] == false && !fc_so->keys[fc_i].excludeOnly)
			return false;
	}

	return true;
}

/*
 * 将所有匹配的行从待处理列表收集到位图中。
 */
static void fc_scanPendingInsert(IndexScanDesc fc_scan, TIDBitmap *fc_tbm, int64 *fc_ntids)
{
	GinScanOpaque fc_so = (GinScanOpaque) fc_scan->opaque;
	MemoryContext fc_oldCtx;
	bool		fc_recheck,
				fc_match;
	int			fc_i;
	pendingPosition fc_pos;
	Buffer		fc_metabuffer = ReadBuffer(fc_scan->indexRelation, GIN_METAPAGE_BLKNO);
	Page		fc_page;
	BlockNumber fc_blkno;

	*fc_ntids = 0;

	/*
	 * 在元页上获取谓词锁，以防止与任何快速更新的
	 * 插入发生冲突。
	 */
	PredicateLockPage(fc_scan->indexRelation, GIN_METAPAGE_BLKNO, fc_scan->xs_snapshot);

	LockBuffer(fc_metabuffer, GIN_SHARE);
	fc_page = BufferGetPage(fc_metabuffer);
	TestForOldSnapshot(fc_scan->xs_snapshot, fc_scan->indexRelation, fc_page);
	fc_blkno = GinPageGetMeta(fc_page)->head;

	/*
	 * 在解锁元页之前获取列表头。头页必须被固定
	 * 以防止被清理过程删除。
	 */
	if (fc_blkno == InvalidBlockNumber)
	{
		/* 没有待处理列表，因此正常扫描 */
		UnlockReleaseBuffer(fc_metabuffer);
		return;
	}

	fc_pos.pendingBuffer = ReadBuffer(fc_scan->indexRelation, fc_blkno);
	LockBuffer(fc_pos.pendingBuffer, GIN_SHARE);
	fc_pos.firstOffset = FirstOffsetNumber;
	UnlockReleaseBuffer(fc_metabuffer);
	fc_pos.hasMatchKey = palloc(sizeof(bool) * fc_so->nkeys);

	/*
	 * 对每个堆行循环。scanGetCandidate返回完整行或行的
	 * 元组来自第一页。
	 */
	while (fc_scanGetCandidate(fc_scan, &fc_pos))
	{
		/*
		 * 检查元组中的条目并设置entryRes数组。
		 *
		 * 如果属于当前堆行的待处理元组分散在多个页面上，
		 * collectMatchesForHeapRow将读取所有这些页面。
		 */
		if (!fc_collectMatchesForHeapRow(fc_scan, &fc_pos))
			continue;

		/*
		 * 一行的条目匹配已完成，因此使用
		 * 一致性函数检查行。
		 */
		fc_oldCtx = MemoryContextSwitchTo(fc_so->tempCtx);
		fc_recheck = false;
		fc_match = true;

		for (fc_i = 0; fc_i < fc_so->nkeys; fc_i++)
		{
			GinScanKey	fc_key = fc_so->keys + fc_i;

			if (!fc_key->boolConsistentFn(fc_key))
			{
				fc_match = false;
				break;
			}
			fc_recheck |= fc_key->recheckCurItem;
		}

		MemoryContextSwitchTo(fc_oldCtx);
		MemoryContextReset(fc_so->tempCtx);

		if (fc_match)
		{
			tbm_add_tuples(fc_tbm, &fc_pos.item, 1, fc_recheck);
			(*fc_ntids)++;
		}
	}

	pfree(fc_pos.hasMatchKey);
}


#define GinIsVoidRes(s)		( ((GinScanOpaque) fc_scan->opaque)->isVoidRes )

int64 gingetbitmap(IndexScanDesc fc_scan, TIDBitmap *fc_tbm)
{
	GinScanOpaque fc_so = (GinScanOpaque) fc_scan->opaque;
	int64		fc_ntids;
	ItemPointerData fc_iptr;
	bool		fc_recheck;

	/*
	 * 设置扫描键，并检查不可满足的查询。
	 */
	ginFreeScanKeys(fc_so);		/* 现在应该没有键，但只是为了确保 */
	ginNewScanKey(fc_scan);

	if (GinIsVoidRes(scan))
		return 0;

	fc_ntids = 0;

	/*
	 * 首先，扫描待处理列表并将任何匹配的条目收集到
	 * 位图中。在我们扫描一个待处理条目之后，其他后端可能会将其
	 * 安排到主索引中，因此我们可能在主扫描中第二次访问它。
	 * 这没关系，因为我们只会在位图中重置相同的位。
	 * （重复访问的可能性是GIN无法支持amgettuple API的主要原因之一。）请注意，
	 * 在扫描主索引之前扫描待处理列表是不合适的，因为并发
	 * 清理可能会使我们完全错过条目。
	 */
	fc_scanPendingInsert(fc_scan, fc_tbm, &fc_ntids);

	/*
	 * 现在扫描主索引。
	 */
	fc_startScan(fc_scan);

	ItemPointerSetMin(&fc_iptr);

	for (;;)
	{
		CHECK_FOR_INTERRUPTS();

		if (!fc_scanGetItem(fc_scan, fc_iptr, &fc_iptr, &fc_recheck))
			break;

		if (ItemPointerIsLossyPage(&fc_iptr))
			tbm_add_page(fc_tbm, ItemPointerGetBlockNumber(&fc_iptr));
		else
			tbm_add_tuples(fc_tbm, &fc_iptr, 1, fc_recheck);
		fc_ntids++;
	}

	return fc_ntids;
}
